/*
 *  YATUN  - Yet another tunneling application
 *
 *  Michal Demin - 2007
 *
 *  main.cpp
 *  Hlavny program
 *
 */
 

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>

#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>


#include "ConfigParser.h"
#include "c_Plug_IO.h"
#include "SchedQueue.h"

#include "Yatun.h"

#include "IO_TCP.h"
#include "IO_UDP.h"
#include "IO_Tun.h"

#define MAX(x,y) ((x)>(y)?(x):(y))

enum {DISCONNECTED, CONNECTED, WAITING }; 

int quit = 0;

static void
sig_term (int sig)
{
  fprintf (stderr, "Interrupt recieved  !!! \n");
  quit = 1;
}

int
main (int argc, char **argv)
{
  int ret;
  int len;
  int server = 0;
  int reliable = 0;
  int dyntimeout = 0;
  int connected = 0;
  int interface_up = 0;
  int keepalive_sent = 0;
  struct timeval LastPacket = {0,0};	// timestamp kedy bol prijaty posledny packet
  struct timeval TunnelTimeout = {0,0};	// timeout kolko moze byt od posledneho prijateho packetu

  struct sigaction sa;
  Packet *Pack = new Packet;
  ConfigParser *Config = new ConfigParser;
  SchedQueue *sQueue = new SchedQueue;

/* sem sa ulosia mena pluginou */
  char IO1name[NAME_LENGHT];
  char IO2name[NAME_LENGHT];

/* tu budu ulozene mena scriptov */
  char *ScriptUP = new char[100];
  char *ScriptDOWN = new char[100];

/* Handle na IO pluginy */
  c_Plug_IO *IOLocal;		//  - local  
  c_Plug_IO *IORemote;		// - remote

/* variable pre select */
  struct timeval sel_timeout;
  fd_set rfds;
  int maxfd;

  /* variable pre getopt */
  int opt;
  int daemon = 0;
  int daemon_cfg = 0;
  char *ConfigFile = NULL;

//    LList *Chain;
  memset (Pack, 0, sizeof (Packet));

/* parsneme command line */

  while ((opt = getopt (argc, argv, "dc:")) != -1) {
    switch (opt) {
    case 'd':
      daemon = 1;
      break;
    case 'c':
      len = strlen (optarg);
      ConfigFile = new char[len + 1];

      memset (ConfigFile, 0, len + 1);
      memcpy (ConfigFile, optarg, len);
      break;
    default:			/* '?' */
      fprintf (stderr, "Usage: %s [-d] [-c config]\n", argv[1]);
      exit (EXIT_FAILURE);
    }
  }

  if (ConfigFile == NULL) {
    len = strlen (DEFAULT_CONFIG);
    ConfigFile = new char[len + 1];

    memset (ConfigFile, 0, len + 1);
    memcpy (ConfigFile, DEFAULT_CONFIG, len);

  }
/* parsneme config a nastavime IO */
  if (Config->Open (ConfigFile) == -1) {
    fprintf (stderr, "Cannot open config file \n");
    exit (-1);
  }

  if (Config->GetSection ("main")) {
    fprintf (stderr, "Section [Main] does not exist !!! \n");
    exit (-1);
  }

  if (Config->GetPropertyByType ("local", IO1name, T_STRING)) {
    printf ("No local module specified \n");
    exit (-1);
  }

  if (Config->GetPropertyByType ("remote", IO2name, T_STRING)) {
    printf ("No remote module specified\n");
    exit (-1);
  }
  Config->GetPropertyByType ("daemon", &daemon_cfg, T_BOOL);
  daemon |= daemon_cfg;

// inicializacia lokalneho modulu
  if (strcmp (IO1name, "tun") == 0) {
    char *Name = new char[100];

    Config->GetSection ("local");
    ret = Config->GetPropertyByType ("name", Name, T_STRING);
    if (ret) {
      IOLocal = new IO_Tun;
    } else {
      IOLocal = new IO_Tun (Name);
    }

    delete[]Name;

  } else {
    printf ("Invalid local module specified .... \n");
    exit (-1);
  }

  if (Config->GetPropertyByType ("script_up", ScriptUP, T_STRING)) {
    printf ("No Up script specified \n");
    delete ScriptUP;

    ScriptUP = NULL;
  }

  if (Config->GetPropertyByType ("script_down", ScriptDOWN, T_STRING)) {
    printf ("No Down script specified \n");
    delete ScriptDOWN;

    ScriptDOWN = NULL;
  }

  ret = IOLocal->Open ();
  fprintf (stderr, "IOLocal '%s':  %s\n", IOLocal->Name, (ret) ? "Failed ..." : "OK");
  if (ret) {
    exit (-1);
  }
// inicializacia remotneho spojenia 
  if (strcmp (IO2name, "tcp") == 0) {
    char *RemoteAddr = new char[100];
    struct sockaddr_in remoteaddr;
    struct sockaddr_in localaddr;
    int port = 0;

    IO_TCP *tcpIO;

    Config->GetSection ("remote");
    Config->GetPropertyByType ("server", &server, T_BOOL);
    ret = Config->GetPropertyByType ("port", &port, T_INT);

    if (ret == -1) {
      port = DEFAULT_PORT;
    }

    localaddr.sin_family = AF_INET;
    localaddr.sin_port = htons (port);
    localaddr.sin_addr.s_addr = INADDR_ANY;

    IORemote = tcpIO = new IO_TCP (&localaddr);

    printf ("Mode '%s' selected\n", server ? "server" : "client");

    // do connect to server 
    if (server == 0) {
      Config->GetPropertyByType ("remote_addr", RemoteAddr, T_STRING);
      memset (&remoteaddr, 0, sizeof (struct sockaddr_in));
      remoteaddr.sin_family = AF_INET;
      remoteaddr.sin_port = htons (port);
      remoteaddr.sin_addr.s_addr = inet_addr (RemoteAddr);
      fprintf (stderr, "Setting Remote Host to : '%s:%d' \n", RemoteAddr, port);
      tcpIO->SetAddress (&remoteaddr);
    } else {
      tcpIO->Listen ();
    }

    delete[]RemoteAddr;
  } else if (strcmp (IO2name, "udp") == 0) {
    char *RemoteAddr = new char[100];
    struct sockaddr_in remoteaddr;
    struct sockaddr_in localaddr;
    int port = 0;

    IO_UDP *udpIO;

    Config->GetSection ("remote");
    Config->GetPropertyByType ("server", &server, T_BOOL);
    Config->GetPropertyByType ("reliable", &reliable, T_BOOL);
    Config->GetPropertyByType ("dynamictimeout", &dyntimeout, T_BOOL);
    ret = Config->GetPropertyByType ("port", &port, T_INT);

    if (reliable == 1) {
      fprintf (stderr, "enabling packet scheduler for reliable UDP connection ! \n");
    }

    if (ret == 0) {
      port = DEFAULT_PORT;
    }

    localaddr.sin_family = AF_INET;
    localaddr.sin_port = htons (port);
    localaddr.sin_addr.s_addr = INADDR_ANY;

    IORemote = udpIO = new IO_UDP (&localaddr);

    printf ("Mode '%s' selected\n", server ? "server" : "client");
    // do connect to server 
    if (server == 0) {
      Config->GetPropertyByType ("remote_addr", RemoteAddr, T_STRING);
      memset (&remoteaddr, 0, sizeof (struct sockaddr_in));
      remoteaddr.sin_family = AF_INET;
      remoteaddr.sin_port = htons (port);
      remoteaddr.sin_addr.s_addr = inet_addr (RemoteAddr);
      fprintf (stderr, "Setting Remote Host to : '%s:%d' \n", RemoteAddr, port);
      udpIO->SetAddress (&remoteaddr);
    } else {
      udpIO->Listen ();
    }

    delete[]RemoteAddr;
  } else {
    printf ("Invalid remote module specified .... \n");
    exit (-1);
  }


  ret = IORemote->Open ();
  fprintf (stderr, "IORemote '%s':  %s\n", IORemote->Name, (ret) ? "Failed ..." : "OK");
  if (ret) {
    exit (-1);
  }
  
//zostrojime chain
//    Chain = new LList;


/* nastavime SIG handler */
  memset (&sa, 0, sizeof (sa));
  sa.sa_handler = SIG_IGN;
  sa.sa_flags = SA_NOCLDWAIT;
  sigaction (SIGHUP, &sa, NULL);
  sigaction (SIGQUIT, &sa, NULL);
  sigaction (SIGPIPE, &sa, NULL);
  sigaction (SIGCHLD, &sa, NULL);

  sa.sa_handler = sig_term;
  sigaction (SIGTERM, &sa, NULL);
  sigaction (SIGINT, &sa, NULL);

/* ak pouzivame Scheduler tak sa nastavi tu */
  if (reliable) {
    //struct timeval timeout;
    sQueue->SetIO (IORemote);
    sQueue->SetDynamicTimeout(dyntimeout);
    sQueue->SetDynamicMul (12);
  }

  TunnelTimeout.tv_sec = 2;
  TunnelTimeout.tv_usec = 0;

  sel_timeout.tv_sec = 0;
  sel_timeout.tv_usec = 100000;

  if (daemon) {
    printf ("Demonizing ... \n");
    if (fork () != 0) {
      printf ("Exiting parrent process\n");
      exit (-1);
    }
  }
/* a vkrocime to loopu */
  while (!quit) {

    if (connected == DISCONNECTED) {
      if (server == 0) {
      	/* ak sme klient tak sa musime k serveru pripojit, a ohlasit sa*/
		IORemote->Connect ();
	 	Pack->Flag = YATUN_PACKET_KEEPALIVE;
		printf ("Sending Hello packet .... \n");
		usleep(1000000);
		IORemote->Send (Pack);
      } else {
		/* sme asi server tak cakame na spojnenie */
		connected = WAITING;
		IORemote->Connect ();
      }

    }

    FD_ZERO (&rfds);
    FD_SET (IOLocal->GetFD (), &rfds);
    FD_SET (IORemote->GetFD (), &rfds);
    maxfd = MAX (IORemote->GetFD (), IOLocal->GetFD ()) + 1;
    ret = select (maxfd, &rfds, NULL, NULL, &sel_timeout);
    if (ret == -1) {
      printf ("Select error !! \n");
      exit (-1);
    } else if (ret != 0) {
      if (FD_ISSET (IOLocal->GetFD (), &rfds)) {
        if (reliable && sQueue->IsFull()) {
	    fprintf(stderr, "Queue is full ... !!! \n");
	} else if (IOLocal->Recieve (Pack) > 0) {
	  if (Pack->Flag & YATUN_PACKET_DATA) {
	    // Chain->Process(Pack,0);
	    if (reliable) {
	      sQueue->AddSend (Pack);
	    } else {
	      ret = IORemote->Send (Pack);
	      //fprintf(stderr,"sent packet len = %d \n", ret );
	    }
	  }
	}
      }				// if(FD_IsSET(IOLocal)

      if (FD_ISSET (IORemote->GetFD (), &rfds)) {

	if (IORemote->Recieve (Pack) > 0) {
	  gettimeofday (&LastPacket, NULL);
	  // printf("IO2->IO1 : %d\n", ret);
	  if (Pack->Flag & YATUN_PACKET_DATA) {
	    // Chain->Process(Pack,1);
	    IOLocal->Send (Pack);
	    if (reliable) {
	      //fprintf(stderr, "Sending ACK for seq = %d \n",Pack->SeqNum);
	      Pack->Lenght = 0;
	      Pack->Flag = YATUN_PACKET_ACK;	// a posleme ack, ze mame packet
	      IORemote->Send (Pack);	// ack sa neposiela cez Scheduler
	    }
	  } else if (Pack->Flag & YATUN_PACKET_KEEPALIVE) {
	    Pack->Flag = YATUN_PACKET_KEEPREPLY;
	    Pack->Lenght = 0; 
	    IORemote->Send (Pack);
	    connected = CONNECTED;
	    //fprintf (stderr, "Sent KEEPALIVE reply !!! \n");
	  } else if (Pack->Flag & YATUN_PACKET_KEEPREPLY) {
	  	keepalive_sent = 0;	// vypneme semafor
	  	connected = CONNECTED;
	  	//fprintf(stderr,"recieved reply for KEEPALIVE\n");
	  } else if (Pack->Flag & YATUN_PACKET_ACK) {
	    if (reliable) {
	      //fprintf(stderr, "recieved ack for seq = %d \n", Pack->SeqNum);
	      sQueue->Remove (Pack->SeqNum);	// dostali sme ack ze packet dosiel
	    }
	  }
	}
      }				// if(FD_ISSET(IORemote)
    }				// if (ret != 0)
    
    /* skontrolujeme timeout  */
    if (connected == CONNECTED) {
      struct timeval curTime;
      curTime.tv_sec = 0; // potesime valgrind 
      curTime.tv_usec = 0; 
      gettimeofday (&curTime, NULL);
      timersub (&curTime, &LastPacket, &curTime);
      if (timercmp (&curTime, &TunnelTimeout, >)) {
      	//fprintf(stderr, "timeout handler ... \n");
		if (keepalive_sent == 0) {
		  // posledna sanca sa zachranit ... natiahneme znova budik, a ak neprijde odpoved tak sa odpojime
		  keepalive_sent = 1;
		  gettimeofday (&LastPacket, NULL);
		  Pack->Lenght = 0;
		  Pack->Flag = YATUN_PACKET_KEEPALIVE;
		  //fprintf(stderr,"KEEPalive sent ... \n");
		  IORemote->Send (Pack);
		} else {
		  //fprintf(stderr,"KEEPalive timeout  disconnected ... \n");
		  keepalive_sent = 0;
		  connected = DISCONNECTED;
		}
      }
    }

    if ((connected == CONNECTED) && !interface_up) {
      interface_up = 1;
      if (ScriptUP) {
		fprintf (stderr, "Executing '%s' ... \n", ScriptUP);
		system (ScriptUP);
      }
    }

    if ((connected == DISCONNECTED) && interface_up) {
      interface_up = 0;
      if (ScriptDOWN) {
		fprintf (stderr, "Executing '%s' ... \n", ScriptDOWN);
		system (ScriptDOWN);
      }
    }

    if (reliable && connected == CONNECTED) {
      sQueue->Check ();		// preposli stratene packety
    }

    if (reliable && connected == DISCONNECTED) {
      sQueue->FlushQueue ();
    }

    usleep (1);
  }

  if (ScriptDOWN && interface_up) {
    system (ScriptUP);
  }

  delete[]ConfigFile;
  delete Config;
  delete IOLocal;
  delete IORemote;
  delete Pack;
  delete sQueue;

  if (ScriptUP)
    delete[]ScriptUP;
  if (ScriptDOWN)
    delete[]ScriptDOWN;

//    delete Chain;
  return 0;
}
